﻿using System.Diagnostics;
using System.Threading.Tasks;

namespace System.IO
{
    internal static class StreamExtensions
    {
        public static int ReadUInt16(this Stream s)
        {
            int b1, b2;
            b1 = s.ReadByte();
            b2 = s.ReadByte();
            if (b2 == -1 || b1 == -1)
                return -1;
            if (BitConverter.IsLittleEndian)
                return b2 << 8 | b2;
            else
                return b1 << 8 | b2;
        }

        public static int ReadNetworkUInt16(this Stream s)
        {
            int b1, b2;
            b1 = s.ReadByte();
            b2 = s.ReadByte();
            if (b2 == -1 || b1 == -1)
                return -1;
            return b1 << 8 | b2;
        }

        public static void WriteUInt16(this Stream s, ushort data)
        {
            s.Write(BitConverter.GetBytes(data), 0, 2);
        }

        public static void WriteNetworkUInt16(this Stream s, ushort data)
        {
            s.WriteByte((byte)(data >> 8 & 0xFF));
            s.WriteByte((byte)(data & 0xFF));
        }

        public static long ReadUInt32(this Stream s)
        {
            int b1, b2, b3, b4;
            b1 = s.ReadByte();
            b2 = s.ReadByte();
            b3 = s.ReadByte();
            b4 = s.ReadByte();
            if (b4 == -1 || b3 == -1 || b2 == -1 || b1 == -1)
                return -1;
            if (BitConverter.IsLittleEndian)
                return b4 << 24 | b3 << 16 | b2 << 8 | b1;
            else
                return b1 << 24 | b2 << 16 | b3 << 8 | b4;
        }


        public static long ReadNetworkUInt32(this Stream s)
        {
            int b1, b2, b3, b4;
            b1 = s.ReadByte();
            b2 = s.ReadByte();
            b3 = s.ReadByte();
            b4 = s.ReadByte();
            if (b4 == -1 || b3 == -1 || b2 == -1 || b1 == -1)
                return -1;
            return b1 << 24 | b2 << 16 | b3 << 8 | b4;
        }

        public static void WriteUInt32(this Stream s, uint data)
        {
            s.Write(BitConverter.GetBytes(data), 0, 4);
        }

        public static void WriteNetworkUInt32(this Stream s, uint data)
        {
            s.WriteByte((byte)(data >> 24 & 0xFF));
            s.WriteByte((byte)(data >> 16 & 0xFF));
            s.WriteByte((byte)(data >> 8 & 0xFF));
            s.WriteByte((byte)(data & 0xFF));
        }

        public static int Read(this Stream s, byte[] data)
        {
            return s.Read(data, 0, data.Length);
        }
        public static Task<int> ReadAsync(this Stream s, byte[] data)
        {
            return s.ReadAsync(data, 0, data.Length);
        }

        public static void Write(this Stream s, byte[] data)
        {
            s.Write(data, 0, data.Length);
        }
        public static Task WriteAsync(this Stream s, byte[] data)
        {
            return s.WriteAsync(data, 0, data.Length);
        }

        public static void CopyBytesTo(this Stream source, Stream destination, int length)
        {
            CopyBytesTo(source, destination, length, 4096);
        }

        public static int CopyBytesTo(this Stream source, Stream destination, int length, int bufferSize)
        {
            byte[] buffer = new byte[bufferSize];
            return CopyBytesTo(source, destination, length, buffer);
        }

        public static int CopyBytesTo(this Stream source, Stream destination, int length, byte[] buffer)
        {
            if (length < 0)
                throw new ArgumentOutOfRangeException(nameof(length));
            
            int bytesCopyed = 0;
            int bufferSize = buffer.Length;
            int thisRoundCopyed;

            do
            {
                int nextLen = length - bytesCopyed;
                if (nextLen > bufferSize) nextLen = bufferSize;
                thisRoundCopyed = source.Read(buffer, 0, nextLen);
                destination.Write(buffer, 0, nextLen);
                if (thisRoundCopyed < nextLen)
                    return bytesCopyed + thisRoundCopyed;
                bytesCopyed += nextLen;
            } while (bytesCopyed < length);
            return bytesCopyed;
        }

        public static Task<long> CopyBytesToAsync(this Stream source, Stream destination, int length)
        {
            return CopyBytesToAsync(source, destination, length, 4096);
        }
        public static async Task<long> CopyBytesToAsync(this Stream source, Stream destination, int length, int bufferSize)
        {
            if (!source.CanRead)
                throw new InvalidOperationException("source must be open for reading");
            if (!destination.CanWrite)
                throw new InvalidOperationException("destination must be open for writing");
            if (length < 0)
                throw new ArgumentOutOfRangeException(nameof(length));
            if (length == 0)
                return 0;

            byte[][] buf = { new byte[bufferSize], new byte[bufferSize] };
            int[] bufl = { 0, 0 };
            int bufno = 0;
            Task<int> read = null;
            Task write = null;

            if (buf[bufno].Length > length)
                read = source.ReadAsync(buf[bufno], 0, (int)length);
            else
                read = source.ReadAsync(buf[bufno], 0, buf[bufno].Length);

            long totalCopyed = 0;

            while (true)
            {

                await read.ConfigureAwait(false);
                bufl[bufno] = read.Result;

                // if zero bytes read, the copy is complete
                if (bufl[bufno] == 0)
                {
                    break;
                }

                // wait for the in-flight write operation, if one exists, to complete
                // the only time one won't exist is after the very first read operation completes
                if (write != null)
                {
                    await write.ConfigureAwait(false);
                }

                // start the new write operation
                write = destination.WriteAsync(buf[bufno], 0, bufl[bufno]);

                totalCopyed += bufl[bufno];

                // toggle the current, in-use buffer
                // and start the read operation on the new buffer.
                //
                // Changed to use XOR to toggle between 0 and 1.
                // A little speedier than using a ternary expression.
                bufno ^= 1; // bufno = ( bufno == 0 ? 1 : 0 ) ;

                if (totalCopyed == length)
                    break;
                if (totalCopyed + buf[bufno].Length > length)
                    read = source.ReadAsync(buf[bufno], 0, (int)(length - totalCopyed));
                else
                    read = source.ReadAsync(buf[bufno], 0, buf[bufno].Length);
            }

            // wait for the final in-flight write operation, if one exists, to complete
            // the only time one won't exist is if the input stream is empty.
            if (write != null)
            {
                await write.ConfigureAwait(false);
            }

            await destination.FlushAsync().ConfigureAwait(false);

            // return to the caller ;
            return totalCopyed;
        }

        public static byte[] GetBuffer(this MemoryStream stream)
        {
            ArraySegment<byte> segment;
            if (stream.TryGetBuffer(out segment))
            {
                Debug.Assert(segment.Offset == 0);
                return segment.Array;
            }
            return Array.Empty<byte>();
        }

        public static byte[] GetBuffer(this MemoryStream stream, int start, int length)
        {
            ArraySegment<byte> segment;
            if (stream.TryGetBuffer(out segment))
            {
                Debug.Assert(segment.Offset == 0);
                if (segment.Array.Length > length)
                {
                    var bytes = new byte[length];
                    Array.Copy(segment.Array, bytes, length);
                    return bytes;
                }
                else
                {
                    return segment.Array;
                }
            }
            return Array.Empty<byte>();
        }
    }
}
